--[[
Name: BrownieHelper
Developed by: Sonora (The Dragon Flight)
Website: http://sonora.kirintor.googlepages.com/contents
SVN: http://code.google.com/p/browniehelper/source/browse
License: GNU General Public License v3
]]

--[[
~Bidding systems overview~

The first point of entry into a bidding system for the BronwieHelper application
is a call which will return one or more bidding systems to use to handle bidding
on a loot item.  This call should be provided as:

getBiddingSystems(item_name, app): 
    item_name --plain name string (no item name encoding)
    app --BrownieHelper application instance doing the requesting
    
    returns an ordered sequence of BiddingSystem tables (can be >=0)
    
There's of course a default implementation of this call provided here which
tries to find a bidding system with a name that matches the loot type of the
given item, and if it can't find one defaults to a standard auction sequence.

If this doesn't work for your Guild's auction system just provide a new
implementation of this call!

The second way you can customize BrownieHelper is by defining new bidding
systems (just tables which provide certain anticipated keys).  See the sections
below for a description of what a bidding system table can look like.

If you'd like your bidding systems to show up in the in-game lists, you should
also call registerBiddingSystem(name, biddingsystem) for each.


~More information~

This module holds tool kit functions and several example bidding systems. For
a full-fledged custom bidding system implementation see browniepoints.lua.


~Data structures~

A bidding system is simply a table which defines:

BiddingSystem = {
    minbid = function(charactername, bidsetup) (required)
        --nil indicates that the player cannot bid; defaults to 0
        --if both minbid==0 and maxbid==0 no bidding instructions or bid window will be shown
        return int | nil end
        
    maxbid = function(charactername, bidsetup) (required)
        --nil indicates that there is no maximum bid for this player; defaults to nil
        --if both minbid==0 and maxbid==0 no bidding instructions or bid window will be shown
        return int | nil end
        
    checkBid = function(bid, bidsetup) (optional)
        --optional checking above and beyond min/max bid requirements;
        --result sent to the bidding player. Defaults to true
        return (true | false ), [response_string] end 
        
    findWinner = function(bids, bidsetup) (required)
        --nil indicates no winner can be found yet; winningbid is a bid structure;
        return (winningbid | nil), [response string] end 
        
    registerWinner = function(winningbid, bidsetup) (optional)
        --fired when the winner is officially set & announced
        return nil 
        
    rollrange = {lower int, upper int} (optional)
        --specifies the allowed roll range, defaults to the standard
        {1,100}
    
    instructions = str --instructions (required)
}

Other structures used here are:

Bid = {
    charactername = str,
    bid = int,
    roll = int | nil, --may not be set
    update = function() return nil  --all client code should call this function
                                    --if they mutate any of the other entries in place
}

Bids = {bid,...}

BidSetup = {
    itemname = str --plain item name string
    itemlink = str --special item link encoded string
    minbid = int --minimum bid, value originates with the MinimumBids but may be tweaked by the user
    phase = int -- 1 to max phase #, nil indicates no bidding is open
    biddingsystem = biddingsystem --bidding system currently in use
    app = BrownieHelper --application instance currently running the bid
}

A note about character v.s. player names:  in BrownieHelper character name
refers to the actual name of a character in the current group, player name is
used to refer to a player's main character, so if someone is in the group on
an alt player name will differ from character name. Call:

BrownieHelper:getPlayerName(charactername)

..if your bidding system needs the player name instead of the character name.
Or:

BrownieHelper:getCharacterName(playername)

to perform the complimentary conversion.

]]--

--Default implementation of getBiddingSystems
function getBiddingSystems(item_name, app)
    --[[
    TDF implementation passes off any piece of loot not explicity associated
    with a loot type in the db to the 4-phase boss loot sequence.
    
    Note: if you're using this addon with the helper script than the relevant
    list of loot items to type associations is setup in the script configuration
    files (usually BrownieHelperGuild.cfg).
    ]]--

    --Grab the table of item name associations
    local lootTypes = app:getLootTypesMap()

    for type,items in pairs(lootTypes) do
        for i, name in ipairs(items) do
            if item_name==name then return {BiddingSystems[type],} end
        end
    end
    
    --If no matches happened above, just compile and return the default sequence of systems
    local sequence = {}
    for i,name in pairs(app:getDb().Options.DefaultBiddingSequence) do
        table.insert(sequence, BiddingSystems[name])
    end
    return sequence
    
end

--Utilities that make life easier when writing bidding systems
function getPlayerPoints(charactername, bidsetup)
    --returns int, the number of points the player has in the current run's pool
    local runName, pointsDb = bidsetup.app:getCurrentPointPool()
    return tonumber((pointsDb[bidsetup.app:getPlayerName(charactername)] or "0"))
end

function getPlayerMaxBid(charactername, bidsetup)
    --Here we define the player's maximum bid as the greater of: the player's point total or the current minimum bid
    local points = getPlayerPoints(charactername, bidsetup)
    if points > bidsetup.minbid then return points
    else return bidsetup.minbid
    end
end

function findBidWinner(bids, bidsetup)
    --returns [winningbid], [{bid,} --possible_winners], [int --second_highest_bid]
    
    --If we don't have any bids
    if #bids == 0 then return nil end
    
    --Assemble a table:  bid_map = { bid_value : {bid,}, }
    local bid_map = {}
    for i,bid in pairs(bids) do
        if bid.bid~=nil then
            if bid_map[bid.bid]==nil then bid_map[bid.bid] = {} end
            table.insert(bid_map[bid.bid], bid)
        end
    end
    
    --Assemble a sorted index of bid values from bid_map
    local sorted_values = {}
    for val,bidlist in pairs(bid_map) do table.insert(sorted_values, val) end
    table.sort(sorted_values, function(a,b) return a>b end) --sorts in decending order

    --Get the winning bid list
    local possible_winners = bid_map[sorted_values[1]]
    
    --If we have a single winner
    if #possible_winners==1 then
        winningbid = possible_winners[1]
        return winningbid, nil, sorted_values[2]
        
    --Otherwise, just return nil since we didn't have a definitive winner.
    else
        return nil, possible_winners, sorted_values[2]
    end
end

function findRollWinner(bids, bidsetup)
    --returns [winningbid], [{bid,} --possible_winners], [int --second_highest_roll]
    
    --If we don't have any bids
    if #bids==0 then return nil end
    
    --Assemble a table: roll_map = { roll_value : {bid,}}
    local roll_map = {}
    for i,bid in pairs(bids) do
        if bid.roll~=nil then
            if roll_map[bid.roll]==nil then roll_map[bid.roll] = {} end
            table.insert(roll_map[bid.roll], bid)
        end
    end
    
    --Assemble a sorted index of roll values from roll_map
    local sorted_rolls = {}
    for v in pairs(roll_map) do table.insert(sorted_rolls, v) end
    table.sort(sorted_rolls, function(a,b) return a>b end) --sort in descending order
    
    --if we didn't have any rolls
    if sorted_rolls[1]==nil then return nil, bids end
    
    --Get the possible winner list
    local possible_winners = roll_map[sorted_rolls[1]]
    
    --If there is a single winner
    if #possible_winners==1 then
        winningbid = possible_winners[1]
        return winningbid, nil, sorted_rolls[2]
    
    --If there's a tie for the winner
    else
        return nil, possible_winners, sorted_rolls[2]
    end
    
end

function adjustWinningBid(winningbid, second_highest_bid, minbid, offset)
    --If the bid is changed, we return a new winning bid object, we do not mutate the passed winningbid.
    --offset is how many points over the next highest bid the winner must bid to win; defaults to 1

    --Assume an offset of 1 if not told otherwise
    if offset==nil then offset=1 end
    
    --if second_highest_bid wasn't nil, but was higher than or equal to the minimum bid, adjust it to second_highest_bid +1
    if second_highest_bid~=nil and second_highest_bid>=minbid then return {bid=second_highest_bid+offset, charactername=winningbid.charactername, roll=winningbid.roll}
    
    --Otherwise if a minbid was given, return it
    elseif minbid~=nil then return {bid=minbid, charactername=winningbid.charactername, roll=winningbid.roll}
    
    --Otherwise, just return the original value
    else return winningbid
    
    end
end

function progressiveFindWinner(bids, bidsetup, minbid)
    --[[
    A progressive winner finding scheme:
    
    1) if there's a clear highest bidder, they win and their bid is adjusted to the minbid or the next highest bid +1
    2) if there are multiple bid winners, see if there's a winner based on rolls
    ]]--
    
    --If we're not passed minbid assume the instance minbid saved on bidsetup
    if minbid==nil then minbid = bidsetup.minbid end
    
    --See if there's a clear winner from the bids
    winningbid, possible_winners, second_highest_bid = findBidWinner(bids, bidsetup)
    
    --if there was a clear winning bid return it
    if winningbid~=nil then return adjustWinningBid(winningbid, second_highest_bid, minbid), ""..winningbid.charactername.." won the bid."
    else
        --if there's a list of possible, see if we can get the winner from rolls
        if possible_winners~=nil then
            winningbid, possible_roll_winners, second_highest_roll = findRollWinner(possible_winners, bidsetup)
            
            --If there's a clear winner, return it
            if winningbid~=nil then return winningbid, ""..winningbid.charactername.. " won the roll."
            else
                --Start watching rolls
                return nil, "The following players should roll off:\n\n"..valueJoin(fromValues(possible_roll_winners,'charactername'),"\n")
            end
            
        else
            return nil, "No bids were found."
        end
    end
    
end

--Here's where we actually define a set of bidding systems; you can just use this table as a library when writing your getBiddingSystems call.
local defaultBiddingSystems = {

    --Here are some example bidding systems (all fully functional!) that demonstrate the attributes that bidding systems can expose
    
    --[[
    A simple auction:
        1.  lets any player place a minimum bid, regardless of their current
            point pool, but doesn't let them bid above the minimum if they don't
            have enough points
        2.  if there's a bid tie the winner is determined by roll off
        3.  the winner is only charged as many points as they would have needed
            to bid to win the auction.
    ]]
    ["Simple Auction"] = {
        minbid = function(charactername, bidsetup) return bidsetup.minbid end,
        maxbid = getPlayerMaxBid,
        findWinner = progressiveFindWinner,
        instructions = "Competitive bidding auction, where ties are determined by roll off, and winners not penalized for bidding higher than they needed to.",
    },
    
    --[[
    Just a basic roll off.  No points involved.
    ]]
    ["Roll off"] = {
        minbid = function() return 0 end,
        maxbid = function() return 0 end,
        findWinner = function(bids, bidsetup)
            --Just check rolls
            winningbid, possible_winners = findRollWinner(bids, bidsetup)
            return winningbid, (possible_winners~=nil and "The following players should roll off:\n\n"..valueJoin(fromValues(possible_winners,'charactername'),"\n") or "")
        end,
        instructions = "This is a simple roll off.  /roll now if you are interested in the item.",
        rollrange={1,100}, --this is the default range, but I show it here as an example
    },

    --[[
    An even simpler auction system that just requires player to bid all of their
    points.
    ]]
    ["All points bid"] = {
        minbid = function(charactername, bidsetup)
            points = getPlayerPoints(charactername, bidsetup)
            return (points>0 and points or nil)
        end,
        maxbid = function(charactername, bidsetup) return getPlayerPoints(charactername, bidsetup) end,
        findWinner = progressiveFindWinner,
        instructions = "This system requires players to always bid all of their points, and disallows bidding if they have none.",
    },
    
    --[[
    Weighted roll systems are popular alternatives to competitive (DKP-like)
    bidding systems.
    ]]
    ["Fixed weighted roll"] = {
        minbid = function() return 0 end,
        maxbid = function() return 0 end,
        findWinner = function(bids, bidsetup)
            --First adjust all of the rolls with each player's points
            for i,bid in pairs(bids) do
                --Note their bid as all of their points
                bid.bid = getPlayerPoints(bid.charactername, bidsetup)
                --If there's a roll, weight it with their points
                if bid.roll~=nil then bid.roll = bid.roll + bid.bid end
                --update() call is required by the framework when we mutate a bid
                bid.update() 
            end
            
            --Now, just check rolls
            winningbid, possible_winners = findRollWinner(bids, bidsetup)
            return winningbid, (possible_winners~=nil and "The following players should roll off:\n\n"..valueJoin(fromValues(possible_winners,'charactername'),"\n") or "")
            
        end,
        instructions = "This is a weighted roll system, where players are required to spend all of their points to weight their rolls.",
    },
    ["Variable weighted roll"] = {
        minbid = function() return 0 end,
        maxbid = getPlayerPoints,
        findWinner = function(bids, bidsetup)
            --First adjust all of the rolls with each player's points
            for i,bid in pairs(bids) do
                --If there's a roll, weight it with their points
                if bid.roll~=nil then bid.roll = bid.roll + (bid.bid or 0) end
                --update() call is required by the framework when we mutate a bid
                bid.update() 
            end
            
            --Now, just check rolls
            winningbid, possible_winners = findRollWinner(bids, bidsetup)
            return winningbid, (possible_winners~=nil and "The following players should roll off:\n\n"..valueJoin(fromValues(possible_winners,'charactername'),"\n") or "")
            
        end,
        instructions = "This is a weighted roll system, where players can spend however many of their points they want to weight their rolls.",
    },
}

--Bidding system registry
BiddingSystems = {}
function registerBiddingSystem(name, biddingsystem)
    --Adds your custom bidding system to the library as name
    if biddingsystem.name==nil then biddingsystem.name = name end
    BiddingSystems[name] = biddingsystem
end
for name,biddingsystem in pairs(defaultBiddingSystems) do registerBiddingSystem(name, biddingsystem) end

